Assertions.php

<?php

namespace Tlf\Tester;

/**
 * See the Exceptions trait for exception-based assertions
 * See the `Other` trait for invert(), disable(), and other methods that are important but are not assertions
 */
trait Assertions {


    /**
     * Call pre-defined global functions as assertions
     *
     * @example Use PHP's `in_array()` function as a test: `$this->in_array('value', $array_to_test);`
     */
    public function __call($method, $args){
        if (!function_exists($method)){
            throw new \Exception("'$method' is not a valid assertion, but you can use any existing function (such as in_array()) as an assertion if it returns truthy/falsey values.");
        }

        $didPass = $method(...$args);
        $this->handleDidPass($didPass);
        $c = count($args);
        echo "'$method'([$c arguments]) ";

    }

    public function isInstanceOf($object, $shouldBe){
            $class = get_class($object);
        if ($object instanceof $shouldBe){
            $this->handleDidPass(true);
            echo "'$class' is instanceof '$shouldBe'";
        } else {
            $this->handleDidPass(false);
            echo "'$class' is NOT instanceof '$shouldBe'";
        }
    }
    public function is_object($object){
        if (is_object($object)){
            $this->handleDidPass(true);
            echo "Is an object";
        } else {
            $this->handleDidPass(false);
            echo "Is not an object";
        }
    }

    public function is_false($value){
        $this->handleDidPass($value === false);
        echo "is_false()";
    }
    public function is_true($value){
        $this->handleDidPass($value === true);
        echo "is_true()";
    }

    /**
     * @param $str the string to check within
     * @param $target a target string (or array of strings) to verify exist within $str
     * @param $strings additional strings to check exist within $str
     */
    public function str_contains($str, $target, ...$strings){
        if (count($strings)>0)$target = [$target,...$strings];
        if (is_array($target)){
            $success = true;
            foreach ($target as $strTarget){
                $oneSuccess = $this->str_contains($str,$strTarget);
                if (!$oneSuccess)$success = false;
            }
            return $success;
        }
        $pass = false;
        if (strpos($str,$target)!==false)$pass=true;

        $this->handleDidPass($pass);
        if ($pass){
            echo "String contains '$target'";
        } else {
            echo "String does not contain '$target'";
        }
        return $pass;
    }

    /**
     * Make sure `$str` does not contain `$target`
     */
    public function str_not_contains(string $str, $target, ...$strings){
        if (count($strings)>0)$target = [$target,...$strings];
        if (is_array($target)){
            $success = true;
            foreach ($target as $strTarget){
                $oneSuccess = $this->str_not_contains($str,$strTarget);
                if (!$oneSuccess)$success = false;
            }
            return $success;
        } else $target = (string)$target;
        $pass = true;
        if (strpos($str,$target)!==false)$pass=false;

        $this->handleDidPass($pass);
        if ($pass){
            echo "String does not contain '$target'";
        } else {
            echo "String contains '$target'";
        }
        return false;
    
    }

    /**
     * Compares bools, strings, files (prefix path with `file://`), arrays, objects, and whatever else. 
     *
     * @param $target the value you want
     * @param $actual the value you have
     * @param $strict true/false for strict (`===`) or non-strict (`==`) comparison. 
     */
    public function compare($target, $actual,$strict=false){
        if (!$strict&&is_string($target)&&substr($target,0,7)=='file://'){
            $file = substr($target,7);
            if (!is_file($file)){
                throw new \Exception("{$target} is not a file. ");
            }
            $ext = substr($file,-4);
            if ($ext=='.php'){
                ob_start();
                require($file);
                $target=ob_get_clean();
            }
            else $target=file_get_contents($file);
        }
        if (!$strict&&is_string($actual)&&substr($actual,0,7)=='file://'){
            $file = substr($actual,7);
            if (!is_file($file)){
                throw new \Exception("{$actual} is not a file. ");
            }
            $ext = substr($file,-4);
            if ($ext=='.php'){
                ob_start();
                require($file);
                $actual=ob_get_clean();
            }
            else $actual=file_get_contents($file);
        }


        if (!$strict&&is_string($target)&&is_string($actual)){
            $target = trim($target);
            $actual = trim($actual);
        }
        $pass = false;
        if ($strict&&($target===$actual))$pass = true;
        else if (!$strict&&$target==$actual)$pass = true;


        $target = $this->comparisonOutput($target);
        $actual = $this->comparisonOutput($actual);
        
        // echo "Strict: ".($strict ? 'true' : 'false');

        $this->handleDidPass($pass);
        if ($strict)echo " strict comparison";
        $this->targetVsActualOutput($target, $actual);

        return $pass;
    }

    /**
     * Simply compare the values 
     * @param $target the value you want
     * @param $actual the value you have
     * @param $strict TRUE to use `===`. FALSE to use `==`
     */
    public function compare_raw($target, $actual, $strict=false){
        $pass = $strict ? $target === $actual
                        : $target == $actual;

        ob_start();
        var_dump($target);
        $target_str = substr(ob_get_clean(), 0,500);
        ob_start();
        var_dump($actual);
        $actual_str = substr(ob_get_clean(), 0,500);

        $this->handleDidPass($pass);
        $this->targetVsActualOutput(
            $target_str,
            $actual_str
        );
        return $pass;
    }

    /**
     * Simply compare two arrays. Prints objects in the array as `ClassName#spl_object_id()`
     *
     * @param $target the array you want
     * @param $actual the array you have
     * @param $strict TRUE to use `===`. FALSE to use `==`
     */
    public function compare_arrays($target, $actual, $strict=false){
        $pass = $strict ? $target === $actual
                        : $target == $actual;

        $target_str = print_r($this->printable_array($target), true);
        $actual_str = print_r($this->printable_array($actual), true);

        $this->handleDidPass($pass);
        $this->targetVsActualOutput(
            $target_str,
            $actual_str
        );
        return $pass;
    }

    /**
     * Simply compare two objects. Prints objects as `ClassName#spl_object_id()`
     *
     * @param $target the object you want
     * @param $actual the object you have
     * @param $strict TRUE to use `===`. FALSE to use `==`
     */
    public function compare_objects($target, $actual, $strict=false){
        $pass = $strict ? $target === $actual
                        : $target == $actual;

        $target_str = !is_object($target) ? 'not-an-object' : get_class($target).'#'.spl_object_id($target);
        $actual_str = !is_object($actual) ? 'not-an-object:'.gettype($actual) : get_class($actual).'#'.spl_object_id($actual);

        $this->handleDidPass($pass);
        $this->targetVsActualOutput(
            $target_str,
            $actual_str
        );
        return $pass;
    }

    /**
     *
     * @return true if tests passes, false otherwise
     */
    public function compare_object_properties(object $object1, object $object2): bool {
        $diff = [];
        $same = [];

        $obj1_id = get_class($object1).'#'.spl_object_id($object1);
        $obj2_id = get_class($object2).'#'.spl_object_id($object2);

        $reflection1 = new \ReflectionObject($object1);
        $reflection2 = new \ReflectionObject($object2);

        // Get all properties, including private and protected ones
        $properties1 = $reflection1->getProperties();
        $properties2 = $reflection2->getProperties();

        foreach ($properties1 as $property) {
            $propertyName = $property->getName();

            // Make private and protected properties accessible
            $property->setAccessible(true);

            $v1 = $property->isInitialized($object1) ? $property->getValue($object1) : "UNDEFINED_OBJECT_PROPERTY";
            $v2 = $property->isInitialized($object2) ? $property->getValue($object2) : "UNDEFINED_OBJECT_PROPERTY";

            if ($v1!=$v2){
                $diff[$propertyName] = [
                    'obj1' => $property->getValue($object1),
                    'obj2' => $property->getValue($object2),
                ];
            } else {
                $same[$propertyName] = 'VALUES MATCH';
            }
        }

        foreach ($properties2 as $property) {
            $propertyName = $property->getName();
            if (isset($diff[$propertyName])
                ||isset($same[$propertyName])
                )continue;

            $v1 = $property->isInitialized($object1) ? $property->getValue($object1) : "UNDEFINED_OBJECT_PROPERTY";
            $v2 = $property->isInitialized($object2) ? $property->getValue($object2) : "UNDEFINED_OBJECT_PROPERTY";

            if ($v1!=$v2){
                $diff[$propertyName] = [
                    'old' => $property->getValue($object1),
                    'new' => $property->getValue($object2),
                ];
            } else {
                $same[$propertyName] = 'VALUES MATCH';
            }
        }

        if (count($diff) == 0){
            echo "\nZero diffs between object properties for $obj1_id and $obj2_id";
            $this->handleDidPass(true);
            return true;
        } 

        echo "\nDiffs exist between object properties on $obj1_id and $obj2_id:\n";
        print_r($diff);
        $this->handleDidPass(false);

        return false;
    }

    public function compare_json($target, $actual, $strict=false){
        if (is_string($target))$target = json_decode($target, true);
        if (is_string($actual))$actual = json_decode($actual, true);
        $this->compare($target, $actual, $strict);
    }


    /**
     * Trim all lines in $str and $target and compare the resulting strings.
     * @param $str The output you have
     * @param $target The output you want
     */
    public function str_contains_lines($str, $target){
        $filter = function($v){return trim($v)!='';};
        $target_lines = explode("\n", $target);
        $target_lines = array_filter($target_lines, $filter);
        $target_lines = array_map('trim', $target_lines);

        $actual_lines = explode("\n", $str);
        $actual_lines = array_filter($actual_lines, $filter);
        $actual_lines = array_map('trim',$actual_lines);

        $actual_lines = array_values($actual_lines);
        $target_lines = array_values($target_lines);

        $target_lines = implode("\n", $target_lines);
        $actual_lines = implode("\n", $actual_lines);
        // $this->targetVsActualOutput($target_lines, $actual_lines);
        $this->str_contains($actual_lines, $target_lines);

    }

    /**
     * Compare all non-empty lines after `trim()`ing each of them.
     * @param $target the lines you want
     * @param $actual the lines you have
     */
    public function compare_lines($target, $actual){
        $filter = function($v){return trim($v)!='';};
        $target_lines = explode("\n", $target);
        $target_lines = array_filter($target_lines, $filter);
        $target_lines = array_map('trim', $target_lines);

        $actual_lines = explode("\n", $actual);
        $actual_lines = array_filter($actual_lines, $filter);
        $actual_lines = array_map('trim',$actual_lines);

        $actual_lines = array_values($actual_lines);
        $target_lines = array_values($target_lines);

        $passed = $target_lines == $actual_lines;

        $this->handleDidPass($passed);
        $target_lines = implode("\n", $target_lines);
        $actual_lines = implode("\n", $actual_lines);
        $this->targetVsActualOutput($target_lines, $actual_lines);

    }
    /**
     * create a dump that converts objects to simple strings referencing their internal object id, so deep nesting is handled
     */
    public function compare_dump($target, $actual){
        $dumped_target = $this->dump_value($target);
        $dumped_actual = $this->dump_value($actual);
        $this->compare($dumped_target, $dumped_actual);
    }
}